package org.hibernate.validator;


import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Iterator;

import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.criterion.ExtendedRestrictions;
import org.hibernate.criterion.Functions;
import org.hibernate.criterion.Restrictions;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.SingleTableSubclass;




public class UnicityValidator implements DatabaseValidator<Unique>, PropertyConstraint, Serializable {

	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private Unique annotation;


	public void initialize(Unique annotation) {
		this.annotation=annotation;
	}

	/**
	 * Decide se evitare di fare la validazione o se invece del effettivamente
	 * interrogare il database.
	 *  
	 * @param bean
	 * @param propertyName
	 * @param value
	 * @return
	 */
	private boolean skipValidation(Object bean, String propertyName, Object value){
		boolean out=false;

		Class<?> clazz=bean.getClass();
		boolean allowNull = !isFieldAnnotatedWith(clazz, propertyName, NotNull.class);
		boolean allowBlank = !isFieldAnnotatedWith(clazz, propertyName,	NotBlank.class);

		//Se value==null allora @NotNull dovrebbe aver segnalato l'errore; 
		//se invece @NotNull non c'è il valore null è valido, quindi lo dovrei controllare
		if (value==null) {
			if (!allowNull) 
				throw new HibernateException(propertyName);
			else
				//value==null è un valore lecito 
				//annotation.nullabled() indica se il vincolo di unicità è da applicare anche al valore null o no
				if (annotation.nullabled())	out=true;
		} else {

			//nel caso stia validando una stringa
			if (value instanceof String) {
				String text = (String) value;
				if (text.trim().isEmpty()) {
					if (!allowBlank) {
						throw new HibernateException(propertyName);
					} else {
						//annotation.nullabled() indica se il vincolo di unicità è da applicare anche alle stringhe vuote
						//nullabled indica che non deve essere fatta alcuna verifica->validazione ok
						if (annotation.nullabled())	out=true;
					}
				}
			}

		}

		return out;

	}

	/**
	 * Metodo che esegue il controllo di unicità del valore value da applicare alla proprietà 
	 * propertyName dell'entità bean. <br />
	 * <b>IMPORTANTE</b> Per evitare inutili query su database, questo 
	 * metodo dovrebbe essere valutato solo se value è sintatticamente corretto.
	 * 
	 * @param bean Entità alla quale appartiene la proprietà
	 * @param propertyName Nome della proprietà alla quale deve essere applicato value
	 * @param value valore da validare
	 * @return true se valido
	 * @author Nicola Spazzoli
	 */
	public boolean isValid(Object bean, String propertyName, Object value, SessionFactory sessionFactory) {
		Session session=null;
		Transaction tx=null;
		if (skipValidation(bean, propertyName, value)) 
			return true;
		try {
			Class<?> clazz=bean.getClass();
//			Session session1 = (Session) Component.getInstance("webkomDatabase");
			session=sessionFactory.openSession();
			tx=session.beginTransaction();
				
			Criteria query = session.createCriteria(clazz);
			if (value==null) {
				query.add(Restrictions.isNull(propertyName));
			} else {
				if (value instanceof String) {
					String text = (String) value;
					if(annotation.trimBeforeValidate()) 
						text=text.trim();
					if (text.isEmpty()) {
						query.add(ExtendedRestrictions.eq(Functions.length(Functions.trim(propertyName)), new Long(0)));
					} else {
						query.add(Restrictions.eq(propertyName, text));
					}
				} else {
					query.add(Restrictions.eq(propertyName, value));
				}
			}

			//eseguo la query
			Object res=null;
			res = query.uniqueResult();
			if (res != null)
				return res.equals(bean);
			else
				return true;
		
		} catch (HibernateException he) {
			if (tx!=null)
				tx.rollback();
			throw he;
		} finally {
			if (session!=null)
				session.close();
		}
		
		
	}




	@Deprecated
	public boolean isValid(Object value) {
		return true;
	}

	/**
	 * Cerca una annotazione sul una determinata proprietà di una classe.
	 * 
	 * @param clazz classe sulla quale cercare
	 * @param propertyName proprietà della classe sulla quale cercare 
	 * @param validator Annotazione da cercare
	 * @return true se l'annotazione viene trovata, false altrimenti
	 */
	private boolean isFieldAnnotatedWith(Class<?> clazz, String propertyName, Class<? extends Annotation> validator){
		Field field = null;
		try {
			field = clazz.getDeclaredField(propertyName);
		} catch (SecurityException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchFieldException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return field.isAnnotationPresent(validator);
	}


	/**
	 * Applica il vincolo di unicità al database. L'applicazione del vincolo sullo script del database 
	 * dipenderà dalla versione del db stesso.
	 */
	public void apply(Property property) {
		if ( ! ( property.getPersistentClass() instanceof SingleTableSubclass ) ) {
			//single table should not be forced to null
			if ( !property.isComposite() ) { //composite should not add not-null on all columns
				Iterator<Column> iter = (Iterator<Column>) property.getColumnIterator();
				while ( iter.hasNext() ) {
					iter.next().setUnique(true);
				}
			}
		}
	}


}
